﻿using EM.RedisCache.AspNetCore.Options;
using gt.rediscache.core;
using gt.rediscache.core.Clients;
using gt.rediscache.core.Connections.ClientServer;
using gt.rediscache.core.Entry;
using gt.rediscache.logger;
using gt.rediscache.plugins;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using System;
using System.Linq;

namespace gt.rediscache.aspnetcore
{
    /// <summary>
    /// 基于Options的RedisClient工厂
    /// </summary>
    internal class OptionsSmartRedisClientFactory : IRedisClientFactory
    {
        private readonly RedisClientsOptions _options;
        private readonly RedisClientServerPoolFactory _poolFactory;
        private readonly RedisClientNotifyer _notifyer;
        private readonly RedisClientStateReporter<RedisClientInfo> _reporter;

        public OptionsSmartRedisClientFactory(IOptionsMonitor<RedisClientsOptions> optionMonitor,
            RedisClientServerPoolFactory poolFactory,
            RedisClientNotifyer notifyer,
            RedisClientStateReporter<RedisClientInfo> reporter)
        {
            _options = optionMonitor.CurrentValue;
            _poolFactory = poolFactory;
            _notifyer = notifyer;
            _reporter = reporter;
        }
        /// <summary>
        /// 创建 RedisClient
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public SmartRedisClient CreateClient(string name)
        {
            var configuration = ConvertToConfiguration(name);
            if (configuration == null) throw new InvalidOperationException($"create redisclient failed by name: {name}.configuration error.");

            return CreateClient(configuration);
        }
        /// <summary>
        /// 创建 RedisClient
        /// </summary>
        /// <param name="configuration"></param>
        /// <returns></returns>
        public SmartRedisClient CreateClient(SmartRedisClientConfiguration configuration)
        {
            var client = new SmartRedisClient(configuration, _poolFactory);
            client.OnClientStateChangedEvent += (sender, args) =>
            {
                var eventClient = sender as IRedisClient;
                if (args.Type == 1 || args.Type == 2)
                {
                    var msg = $"redisclient:{eventClient.AppCacheName} redisClientServer:{args.ClientServer.Name} connect failed.";
                    _notifyer.NotifyAsync(msg, args.ClientServer.Name, args.Type == 2);
                }
                _reporter.ReportAsync(eventClient.RedisCacheInfo());
            };
            //首次创建report
            _reporter.ReportAsync(client.RedisCacheInfo());
            return client;
        }

        private SmartRedisClientConfiguration ConvertToConfiguration(string appCacheName)
        {
            var appCache = _options.ApplicationCaches.FirstOrDefault(x => string.Equals(x.Name, appCacheName, StringComparison.CurrentCultureIgnoreCase));
            if (appCache == null) throw new ArgumentException($"can not find application cache by name:{appCacheName}");

            SmartRedisClientConfiguration configuration = new SmartRedisClientConfiguration();
            configuration.ApplicationName = _options.ApplicationName;

            if (!Enum.TryParse<BackupModeEnum>(appCache.BackupMode, out BackupModeEnum backupMode))
            {
                throw new InvalidOperationException(nameof(BackupModeEnum));
            }
            var delayedRecoverySeconds = -1;
            var redisFailoverWaitSeconds = 5;
            if (!int.TryParse(appCache.DelayedRecoverySeconds, out delayedRecoverySeconds))
            {
                delayedRecoverySeconds = -1;
                RedisLogManager.Warn($"redisclient {appCache.Name} invalid DelayedRecoverySeconds value:{appCache.DelayedRecoverySeconds}");
            }
            if (!int.TryParse(appCache.RedisFailoverWaitSeconds, out redisFailoverWaitSeconds))
            {
                redisFailoverWaitSeconds = 5;
                RedisLogManager.Warn($"redisclient {appCache.Name} invalid RedisFailoverWaitSeconds value:{appCache.RedisFailoverWaitSeconds}");
            }

            configuration.AllowSwitch = backupMode == BackupModeEnum.Switch || backupMode == BackupModeEnum.SwitchAndSync;
            configuration.AllowSync = backupMode == BackupModeEnum.Sync || backupMode == BackupModeEnum.SwitchAndSync;
            configuration.ClientName = appCache.Name;
            configuration.IDC = _options.IDC;

            var mainNodePool = _options.RedisNodePools.FirstOrDefault(x => string.Equals(x.Name, appCache.MainCache, StringComparison.CurrentCultureIgnoreCase));
            if (mainNodePool == null) throw new Exception($"can not find node pool by name:{appCache.MainCache}");
            configuration.Pool = ConvertToCacheConfiguration(configuration.ApplicationName, delayedRecoverySeconds, redisFailoverWaitSeconds, mainNodePool);

            configuration.SwitchNodes = appCache.SwitchNodes;
            if (!string.IsNullOrEmpty(appCache.BackupCache))
            {
                var backupNodePool = _options.RedisNodePools.FirstOrDefault(x => string.Equals(x.Name, appCache.BackupCache, StringComparison.CurrentCultureIgnoreCase));
                if (backupNodePool == null) throw new Exception($"can not find node pool by name:{configuration.ClientName}");

                RedisClientConfiguration backupConfiguration = new RedisClientConfiguration();
                backupConfiguration.ApplicationName = configuration.ApplicationName;
                backupConfiguration.ClientName = appCache.Name;
                backupConfiguration.IDC = configuration.IDC;
                backupConfiguration.Pool = ConvertToCacheConfiguration(configuration.ApplicationName, delayedRecoverySeconds, redisFailoverWaitSeconds, backupNodePool);

                configuration.BackupOptions = backupConfiguration;
            }
            return configuration;
        }
        private CachePoolConfiguration ConvertToCacheConfiguration(string applicationName, int delayedRecoverySeconds, int failoverWaitSeconds, RedisNodePoolOptions option)
        {
            var mode = PoolMode.Single;
            if (!Enum.TryParse<PoolMode>(option.Mode, out mode))
            {
                throw new InvalidOperationException($"invalid node pool mode:{option.Mode}");
            }
            return new CachePoolConfiguration
            {
                Name = option.Name,
                Mode = mode,
                DelayedRecoverySeconds = delayedRecoverySeconds,
                RedisFailoverWaitSeconds = failoverWaitSeconds,
                Nodes = option.Nodes.Select(x =>
                {
                    return new RedisNode
                    {
                        Name = x.Name,
                        Master = x.Master,
                        Slaves = x.Slaves,
                        DB = string.IsNullOrEmpty(x.DB) ? -1 : int.Parse(x.DB),
                        SlotFrom = string.IsNullOrEmpty(x.SlotFrom) ? 0 : int.Parse(x.SlotFrom),
                        SlotTo = string.IsNullOrEmpty(x.SlotTo) ? 0 : int.Parse(x.SlotTo),
                        RedisConnectOptions = ParseToConfiguration(applicationName, x, mode)
                    };
                }).ToList()
            };
        }
        private ConfigurationOptions ParseToConfiguration(string applicationName, RedisNodeOptions node, PoolMode mode)
        {
            ConfigurationOptions configuration = new ConfigurationOptions();
            configuration.AbortOnConnectFail = false;
            configuration.KeepAlive = 10;
            configuration.ConnectTimeout = 1000;
            configuration.ReconnectRetryPolicy = new ExponentialRetry(1000);
            configuration.ConnectRetry = 3;
            configuration.EndPoints.Add(node.Master);
            configuration.AllowAdmin = true;
            if (!string.IsNullOrEmpty(applicationName))
                configuration.ClientName = applicationName;
            if (node.Slaves != null && node.Slaves.Count != 0)
            {
                foreach (var s in node.Slaves)
                {
                    if (!string.IsNullOrEmpty(s))
                        configuration.EndPoints.Add(s);
                }
            }
            if (!string.IsNullOrEmpty(node.Auth))
            {
                configuration.Password = node.Auth;
            }
            if (mode == PoolMode.Sentinel)
            {
                configuration.CommandMap = CommandMap.Sentinel;
                configuration.TieBreaker = string.Empty;
            }
            return configuration;
        }
    }
}
